Support non-unique keys for dictionary column types - #22839
Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
|
/ok to test |
|
/ok to test |
|
are there any existing kernels that assume dictionaries have unique keys? |
I've not found any as of yet. Still working on this. |
|
/ok to test |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughDictionary documentation now allows duplicate keys. Argmin/argmax and minmax reductions now operate on dictionary keys directly via lazy iterators and keys-type dispatch. Tests were added for duplicate-key behavior across dictionary APIs and reductions. ChangesDictionary duplicate-keys support
Estimated code review effort: 4 (Complex) | ~60 minutes Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
mhaseeb123
left a comment
There was a problem hiding this comment.
Approving with minor nits
| auto minimum = make_fixed_width_scalar(key_type, stream, mr); | ||
| auto maximum = make_fixed_width_scalar(key_type, stream, mr); |
There was a problem hiding this comment.
Claude flagged that make_fixed_width_scalar(data_type, ...) goes through scalar_construction_helper, which constructs with is_valid = false.
Also ref: Copilot's concern about decimal scale being dropped seems unhandled
| auto minimum = make_fixed_width_scalar(key_type, stream, mr); | |
| auto maximum = make_fixed_width_scalar(key_type, stream, mr); | |
| auto make_scalar = [&]() -> std::unique_ptr<scalar> { | |
| if constexpr (cudf::is_fixed_point<T>()) { | |
| return make_fixed_point_scalar<T>( | |
| storage_type{}, numeric::scale_type{key_type.scale()}, stream, mr); | |
| } else { | |
| auto scalar = make_fixed_width_scalar(key_type, stream, mr); | |
| scalar->set_valid_async(true, stream); | |
| return std::move(scalar); | |
| } | |
| }; | |
| auto minimum = make_scalar(); | |
| auto maximum = make_scalar(); |
Please also assert validity in the new tests — DictionaryReductionTest.MinMaxUnsortedDuplicateKeys only compares .first / ->value(), and no pre-existing gtest checks is_valid() for a fixed-width dictionary min/max, which is why CI is green on this. DictionaryTypes is also only {int32_t, int64_t, float, double}, so a decimal case would be needed to pin the scale.
There was a problem hiding this comment.
The decimal scale being dropped is a separate issue in this source code (not just dictionary) which I will address in a follow-on PR. The is_valid case is valid and I'll fix that here.
| auto d_dictionary = column_device_view::create(col, stream); | ||
| if (col.has_nulls()) { | ||
| auto pair_to_minmax = cuda::make_transform_iterator( | ||
| cudf::dictionary::detail::make_dictionary_pair_iterator<T>(*d_dictionary, true), |
There was a problem hiding this comment.
Follow-up PR: Perhaps we can we add a benchmark, if none exists, to measure whether loading keys[indices[i]] per row rather than reducing indices and slicing keys affects low-cardinality dictionary minmax performance?
There was a problem hiding this comment.
Yes, some exploration would be good here. This could first do a groupby-count to determine which keys are present (possibly including null values) and then do the minmax reduction over the keys with nonzero counts.
There was a problem hiding this comment.
Approving with minor comments, but we need the null test cases and other pieces that @mhaseeb123 mentioned.
| * Although duplicate keys are allowed, indices in the returned dictionary may | ||
| * only reference one of the duplicates. |
There was a problem hiding this comment.
I read this 3 times before I started to understand it.
What is guaranteed/preserved here? I think the number of keys in the output dictionary is keys.size(), even if keys contains duplicates. But because we have to reassign all the indices to match the new keys, we arbitrarily choose one of those duplicate values to provide the index for reassignment. Do we (or should we) provide any guarantee around which of the duplicates is used for the index? First/last/any?
It is hard to provide determinism and lossless conversion/transformation with the ambiguities introduced by duplicate keys.
Maybe there's a better way to say this, or maybe the current state is fine.
There was a problem hiding this comment.
We introduced non-determinism by no longer requiring sorted keys.
So determinism would be a big requirement and we would have reconsider all of the current dictionary implementation in libcudf again.
| auto d_dictionary = column_device_view::create(col, stream); | ||
| if (col.has_nulls()) { | ||
| auto pair_to_minmax = cuda::make_transform_iterator( | ||
| cudf::dictionary::detail::make_dictionary_pair_iterator<T>(*d_dictionary, true), |
There was a problem hiding this comment.
Yes, some exploration would be good here. This could first do a groupby-count to determine which keys are present (possibly including null values) and then do the minmax reduction over the keys with nonzero counts.
|
/merge |
Description
Updates libcudf DICTIONARY columns to support non-unique keys.
Duplicate keys are support on input for all libcudf APIs but any APIs that return dictionary columns will likely have non-unique keys. The exception to this is the
make_dictionary_columnwhich does not inspect the keys, indices it is given and thecudf::dictionary::set_keys()which always honors the given keys in uniqueness (or not) and order.Checklist